from django.conf import settings
from django.core.paginator import ObjectPaginator, InvalidPage
from django.template import Context, Library, Node, NodeList, Template
from django.template import TemplateSyntaxError, defaultfilters
from django.template import VariableDoesNotExist, resolve_variable
from django.template.loader import get_template
from django.http import QueryDict
from django.utils.datastructures import MultiValueDict

from namedquerysets import NamedQuerySet, ProgrammerError
from namedquerysets import canonicalize_querystring, canonical_querystring
register = Library()

SCOPE_DELIMITER = '.'
def enscope(varname,scope):
    if scope:
        return "%s%s%s" % (scope,SCOPE_DELIMITER,varname)
    else:
        return varname

def _lookup_in_context(varname, context):
    if varname is None: return []
    try:
        return [resolve_variable(varname, context)]
    except VariableDoesNotExist:
        return []
    
def _lookup_in_mvdict(varname, mvdict):
    if varname in mvdict:
        result = mvdict.getlist(varname)
        if len(result) == 1: result = result[0]
        return [result]
    else:
        return []

def _lookup_in_dict(varname, dic):
    if varname in dic:
        return [dic[varname]]
    else:
        return []
    
def _lookup(varname, ctx):
    if isinstance(ctx, Context):
        return _lookup_in_context(varname, ctx)
    elif isinstance(ctx, MultiValueDict):
        return _lookup_in_mvdict(varname, ctx)
    elif isinstance(ctc, dict):
        return _lookup_in_dict(varname, ctx)
    else:
        raise ProgrammerError

def resolve(variable, context, default=None, scope=None):
    """
    Looks up a variable in the context, starting in the nearest scope, and
    then up from there. If the variable is unresolvable all the way up (even
    unscoped), returns default.
    
    context - either a Context, MultiValueDict, or a plain dict.
    """
    scopes = []
    if scope: scopes = scope.split(SCOPE_DELIMITER)
    scopes = [SCOPE_DELIMITER.join(scopes[0:n]) for n in range(len(scopes)+1)]
    for scope in reversed(scopes):
        result = _lookup(enscope(variable,scope), context)
        if result: return result[0]
    return default

class AdjustUrlParamNode(Node):
    def __init__(self, tag_name, params):
        self._tag_name = tag_name
        self._params = params
    
    def render(self, context):
        scope = resolve('template_scope', context)
        querystring = resolve('querystring', context, default='', scope=scope)
        filter = resolve('filter', context, scope=scope)
        params = QueryDict(querystring).copy()
        
        #TODO: should we also remove the reset_params in higher scopes too?
        removable = []
        if filter:
            removable = [enscope(p,scope) for p in filter.reset_params]

        # Adjust parameters.
        for k,v in self._params.iteritems():
            k = enscope(resolve(k, context), scope)
            if v is not None:
                v = resolve(v, context)
            L = params.getlist(k)
            if self._tag_name == 'remove_url_param':
                if v in L:
                    L.remove(v)
                elif v is None:
                    L = []
            elif self._tag_name == 'add_url_param' and v not in L:
                if v is not None:
                    L.append(v)
            else: #set_url_param
                if v is None:
                    L = []
                else:
                    L = [v]
            params.setlist(k,L)
        for k in params.keys():
            if k in removable: del params[k]
            elif not params.getlist(k): del params[k]
        querystring = params.urlencode()
        url = "?%s" % (querystring,)
        return url

def adjust_url_param(parser, token):
    """
    Example: {% set_url_param 'page' next %}
    """
    # split_contents() knows not to split quoted strings.
    bits = list(token.split_contents())
    params = {}
    for n,bit in enumerate(bits[1:]):
        if n % 2:
            v = bit
            params[k] = v
        else:
            k = bit
    else:
        if not (n % 2):
            if bits[0] == 'remove_url_param':
                params[k] = None
            else:
                raise TemplateSyntaxError, "Unable to parse %r tag" % bits[0]
    return AdjustUrlParamNode(tag_name=bits[0], params=params)

set_url_param = register.tag('set_url_param',adjust_url_param)
add_url_param = register.tag('add_url_param',adjust_url_param)
remove_url_param = register.tag('remove_url_param',adjust_url_param)
    
class ViewQuerySetNode(Node):
    def __init__(self, tag_name, slug, template, params='', id=None):
        self.tag_name = tag_name
        self.slug = slug
        self.template_name = template
        self.expected_params = params
        self.id = id
        
    def render(self, context):
        request = resolve('request.GET.urlencode', context, default='')
        scope = resolve('template_scope', context)
        id = resolve(self.id, context, scope=scope)
        scope = enscope(id, scope)
        slug = resolve(self.slug, context, scope=scope)
        template_name = resolve(self.template_name, context, scope=scope)
        if self.expected_params:
            expected_params = \
                resolve(self.expected_params, context, scope=scope)
            expected_params = \
                map(lambda s: s.strip(), expected_params.split(','))
        else:
           expected_params = []
        nqs_expected_params = NamedQuerySet.expected_params(slug)
        params2 = dict((p,resolve(p,QueryDict(request).copy(),scope=scope))
                       for p in expected_params + nqs_expected_params)
        params2 = dict((k,v) for k,v in params2.iteritems() if v is not None)
        
        params = {'querystring': canonicalize_querystring(request)}
        params.update(params2)
        params.update(template_scope=scope)
        context.update(params)
        result = NamedQuerySet.render(slug, context, template_name)
        context.pop()
        return result    



def parse_token(token, expected_args, required_args):
    """
    Returns (tagname, args).
        tagname - a string
        args - a dict of key,value pairs. Split happens at first '=' in each
               pair.
    """
    # expected_args must start with required_args.
    assert (expected_args[0:len(required_args)] == required_args)
    
    # split_contents() knows not to split quoted strings.
    bits = list(token.split_contents())
    tagname = bits[0]
    del bits[0]

    # make sure we got the right number of args.
    if not (len(required_args) <= len(bits) <= len(expected_args)):
        raise TemplateSyntaxError, \
              "%r takes %d to %d arguments" % \
              (tagname,len(required_args), len(expected_args))

    # split them into key,value pairs.
    args = {}
    for n,bit in enumerate(bits):
        parts = bit.split("=",1)
        if len(parts) > 1:
            k = parts[0]
            v = parts[1]
        else:
            k = expected_args[n]
            v = bit
        if k not in expected_args:
            raise TemplateSyntaxError, \
                  "%s has unexpected parameter: %s" % (tagname,k)
        args[k] = v

    # make sure we have all the required args.
    for arg in required_args:
        if arg not in args:
            raise TemplateSyntaxError, "%s missing '%s' param" % (tagname,arg)

    return tagname, args

def view_queryset(parser, token):
    """
    Example:
        {% view_queryset slug='events' template='object_list.html'
                         params='page' id='a' %}
                             
    slug - required, it should be one of the NamedQuerySet slugs.
    template - required.
    params - A comma-separated list of parameters that you want the template
             to be able to read from the querystring. (No need to list filter
             params, they're taken care of automatically)
    id - recommended, view_queryset tags with matching ids share filter params

    Note that in addition to whatever is listed in the 'params' argument,
    the template can also find the following parameters in context:
        querystring - (from request.GET)
        namedqueryset.slug - a string
        namedqueryset.name - a string
        namedqueryset.queryset - a Django QuerySet
        namedqueryset.filters - a sequence of AbstractQuerysetFilter objects
        namedqueryset.filters_by_slug - a dict from slug to filter.
        namedqueryset.filtered.title - the filter-state, human-readable string
        namedqueryset.filtered.queryset - The QuerySet, filtered.
        namedqueryset.filtered.querystring - just the filter state, URLencoded
    """
    expected_args = ("slug", "template", "params", "id")
    required_args = ("slug", "template")
    tag_name, args = parse_token(token, expected_args, required_args)
    return ViewQuerySetNode(tag_name=tag_name, **args)
view_queryset = register.tag(view_queryset)


class FilterUiNode(Node):
    def __init__(self, tag_name, filter, template=None):
        self.tag_name = tag_name
        self.filter = filter
        self.template_name = template
        
    def render(self, context):
        scope = resolve('template_scope',context)
        filter = resolve_variable(self.filter, context)
        template_name = None
        if self.template_name:
            template_name = resolve(self.template_name, context)
        if not template_name:
            template_name = getattr(type(filter),'default_template_name','')
        if not template_name:
            raise VariableDoesNotExist,"Can't render without a template!"
        queryset = resolve('namedqueryset.queryset', context, scope=scope)
        template = get_template(template_name)
        unaliased_params = {}
        for internal_name,external_name in filter.params().iteritems():
            unaliased_params[internal_name] = \
                resolve(external_name,context,scope=scope)
        extra_context = filter._context(queryset, **unaliased_params)
        ctx = dict(unaliased_params)
        ctx.update(extra_context)
        ctx.update(filter=filter)
        context.update(ctx)
        result = template.render(context)
        context.pop()
        return result


def filter_ui(parser, token):
    """
    Renders a User Interface for the filter.

    This tag should only appear within a template that will be rendered by
    a view_queryset templatetag.
    
    Takes a (descendent of) AbstractQuerysetFilter, and optionally the name of
    a template.
    
    The template's context will be updated with the filter's external param
    names re-mapped to their internal names, as well as whatever key-value
    pairs this filter's _context() method generates, and finally, an extra
    param: 'filter', which is the filter object itself.
    
    If no template_name is provided, the filter class's default_template_name
    attribute will be used.
        
    Example:
     {% for filter in namedqueryset.filters %}
        {% ifnotequal filter.slug 'orderby' %}
          {% filter_ui filter %}
        {% endifnotequal %}
     {% endfor %}
     ...
     {% filter_ui namedqueryset.filters_by_slug.orderby 'orderby_filter.html' %}
    """
    expected_args = ("filter","template")
    required_args = ("filter",)
    tag_name, args = parse_token(token, expected_args, required_args)
    return FilterUiNode(tag_name=tag_name, **args)
filter_ui = register.tag(filter_ui)

class PaginateNode(Node):
    def __init__(self, tag_name, nodelist, queryset,
                 paginate_by=0, allow_empty=False, list_name="'object_list'"):
        self.tag_name = tag_name
        self.nodelist = nodelist
        self.queryset = queryset
        self.paginate_by = paginate_by
        self.allow_empty = (allow_empty == 'True')
        self.list_name = list_name
    
    def render(self, context):
        scope = resolve('template_scope', context)
        queryset = resolve(self.queryset, context, scope=scope)
        queryset = queryset._clone()
        paginate_by = resolve(self.paginate_by, context, scope=scope)
        if paginate_by:
            paginate_by = int(paginate_by)
            allow_empty = self.allow_empty
            list_name = resolve(self.list_name, context, scope=scope)
            page = resolve('page', context, default='1', scope=scope)
            paginator = ObjectPaginator(queryset, paginate_by)
            try:
                page = int(page)
                object_list = paginator.get_page(page - 1)
            except (InvalidPage, ValueError):
                if page == 1 and allow_empty:
                    object_list = []
                else:
                    raise Http404
            ctx = {list_name: object_list,
                   'is_paginated': paginator.pages > 1,
                   'results_per_page': paginate_by,
                   'has_next': paginator.has_next_page(page - 1),
                   'has_previous': paginator.has_previous_page(page - 1),
                   'page': page,
                   'next': page + 1,
                   'previous': page - 1,
                   'pages': paginator.pages,
                   'hits': paginator.hits,
                   'page_list': range(1,paginator.pages + 1),
            }
        else:
            ctx = {'paginate_by': None, 'is_paginated': False}
        context.update(ctx)
        result = self.nodelist.render(context)
        context.pop()
        return result
        
def paginate(parser, token):
    """
    Example:
      {% paginate queryset paginate_by=10 allow_empty=True
                           list_name='object_list' %}
    """
    expected_args = ("queryset","paginate_by","allow_empty","list_name")
    required_args = ("queryset",)
    tag_name, args = parse_token(token, expected_args, required_args)
    nodelist = parser.parse(('end%s' % tag_name,))
    parser.delete_first_token()
    return PaginateNode(tag_name, nodelist, **args)
paginate = register.tag(paginate)

class NestedResolveNode(Node):
    def __init__(self, tag_name, var, nesting=1, escape=False):
        self.tag_name = tag_name
        self.var = var
        self.nesting = nesting
        self.escape = (escape == 'True')
    def render(self, context):
        scope = resolve('template_scope', context)
        nesting = resolve(self.nesting, context, scope=scope)
        var = self.var
        for n in range(nesting):
            if var is None: raise VariableDoesNotExist
            var = resolve(var, context, scope=scope)
        if var is None:
            var = ''
        if self.escape:
            var = defaultfilters.escape(var)
        return var
def nested_resolve(parse, token):
    """
    If it were possible, the following would be equivalent:
    {% nested_resolve foo 1 %}  AND  {{ foo }}
    {% nested_resolve foo 2 %}  AND  {{ {{ foo }} }}
    {% nested_resolve foo 3 %}  AND  {{ {{ {{ foo }} }} }}
    
    You can also escape the final result like so:
    {% nested_resolve foo 3 escape=True %}
    """
    expected_args = ("var", "nesting", "escape")
    required_args = ("var",)
    tag_name, args = parse_token(token, expected_args, required_args)
    return NestedResolveNode(tag_name, **args)
nested_resolve = register.tag(nested_resolve)